//------------------------------------------------------------------------------ 
//      Copyright (c) 2004-2009, Be Nanashi-San 
// 
//      Permission is hereby granted, free of charge, to any person obtaining a 
//      copy of this software and associated documentation files (the "Software"), 
//      to deal in the Software without restriction, including without limitation 
//      the rights to use, copy, modify, merge, publish, distribute, sublicense, 
//      and/or sell copies of the Software, and to permit persons to whom the 
//      Software is furnished to do so, subject to the following conditions: 
// 
//      The above copyright notice and this permission notice shall be included in 
//      all copies or substantial portions of the Software. 
// 
//      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
//      IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
//      FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
//      AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
//      LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
//      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
//      DEALINGS IN THE SOFTWARE. 
// 
//      File Name:	ThreadListView.cpp
//      Author: 	Be Nanashi-San 
//      Description:	 
//------------------------------------------------------------------------------
//todo col_hostをメンバ変数に、thread_info, host_info(url_info)新設。
//update処理

#include "config.h"

#include <Alert.h>
#include <Beep.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <FindDirectory.h>
#include <Directory.h>
#include <InterfaceDefs.h>
#include <UTF8.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "2chPositive.h"
#include "DisplayThreadView.h"
#include "NetUtil.h"
#include "ColumnTypes.h"
#include "ThreadListView.h"
#include "Util.h"
#include "datattr.h"
#include "messages.h"
#include "setting.h"
#include "arrows.h"
#include "BBSHandler.h"

enum col_type{
	COL_NO = 0,
	COL_STATUS, //↑↓→●
	COL_THREADNAME,
	COL_RES, // スレ数
	COL_READ,//キャッシュのスレ数
	COL_DATE,//最終更新日付
	COL_DATNAME
};

const unsigned int kMSG_THREADLIST_INVOCATION = 'MTHL';

////////////////////////////////////////////////////////////////////////////////
//  ThreadListView::ThreadListView
//
//
////////////////////////////////////////////////////////////////////////////////
ThreadListView::ThreadListView()
:BColumnListView("ThreadListView", B_NAVIGABLE, B_FANCY_BORDER)
,fCacheDir(new BDirectory())
,fBoardUri(NULL)
{
	this->SetSelectionMode(B_SINGLE_SELECTION_LIST);

	const rgb_color backcolor = {255,255,255,0};
	this->SetColor(B_COLOR_BACKGROUND, backcolor);
	const rgb_color selectioncolor = {152,152,152,0};
	this->SetColor(B_COLOR_SELECTION, selectioncolor);

	this->SetColumns();
	this->SetBitmaps();

  	BMessage msg;
	if(theSettings().GetMessage(kSETTING_THREADLIST, &msg) == B_OK){
		this->LoadState(&msg);
	}

	fTreadRunner = new ThreadRunner<ThreadListView>;
	fStopSem = create_sem(1, "ListReader_sem");
}
////////////////////////////////////////////////////////////////////////////////
// ~ThreadListView
//
//
////////////////////////////////////////////////////////////////////////////////
ThreadListView::~ThreadListView()
{
	BMessage msg;
  	
  	this->SaveState(&msg);
	theSettings().SetMessage(kSETTING_THREADLIST, &msg);
	
	delete fCacheDir;
	delete fBlueUpArrow;
	delete fYellowEqaulArrow;
	delete fRedDownArrow;
	delete fGrayBall;
	delete_sem(fStopSem);
	delete fTreadRunner;

}
////////////////////////////////////////////////////////////////////////////////
// SetBitmaps
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::SetColumns()
{
	float colum_length;
	colum_length = be_plain_font->StringWidth ("No.");
	BIntegerColumn *icol = new BIntegerColumn ("No.", colum_length * 3, 0, colum_length * 3);
  	this->AddColumn (icol, COL_NO);
	this->SetSortColumn(icol, false, true);

	//20050709:990@みんなでBeOS！(Free) 氏により最大幅修正。
	//Ｓは2chTrackerより（Statusの意味か？）
  	this->AddColumn (new BBitmapColumn ("S", kArrowWidth, kArrowWidth, kArrowWidth * 4, B_ALIGN_CENTER), COL_STATUS);

	colum_length = be_plain_font->StringWidth ("あ");
  	this->AddColumn (new BStringColumn ("タイトル", colum_length * 20, 0, colum_length * 40, 0), COL_THREADNAME);

	colum_length = be_plain_font->StringWidth ("新着");
  	this->AddColumn (new BIntegerColumn ("新着", colum_length * 2, 0, colum_length * 2), COL_RES);

	colum_length = be_plain_font->StringWidth ("既得");
  	this->AddColumn (new BIntegerColumn ("既得", colum_length * 2, 0, colum_length * 2), COL_READ);

	colum_length = be_plain_font->StringWidth ("9999/99/99");
  	this->AddColumn (new BStringColumn ("最終取得", colum_length * 2, 0, colum_length * 2, 0), COL_DATE);

  	this->AddColumn (new BStringColumn (NULL, 0, 0, 0, 0), COL_DATNAME);
}
////////////////////////////////////////////////////////////////////////////////
// SetBitmaps
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::SetBitmaps()
{
	fBlueUpArrow = new BBitmap(BRect(0, 0, kArrowWidth - 1, kArrowHeight - 1),  B_CMAP8);
	fBlueUpArrow->SetBits(kBlueUpArrow, sizeof(kBlueUpArrow), 0, B_CMAP8);
	fYellowEqaulArrow = new BBitmap(BRect(0, 0, kArrowWidth - 1, kArrowHeight - 1),  B_CMAP8);
	fYellowEqaulArrow->SetBits(kYellowEqaulArrow, sizeof(kYellowEqaulArrow), 0, B_CMAP8);
	fRedDownArrow = new BBitmap(BRect(0, 0, kArrowWidth - 1, kArrowHeight - 1),  B_CMAP8);
	fRedDownArrow->SetBits(kRedDownArrow, sizeof(kRedDownArrow), 0, B_CMAP8);
	fGrayBall = new BBitmap(BRect(0, 0, kArrowWidth - 1, kArrowHeight - 1),  B_CMAP8);
	fGrayBall->SetBits(kGrayBall, sizeof(kGrayBall), 0, B_CMAP8);
}
////////////////////////////////////////////////////////////////////////////////
// AttachedToWindow
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::AttachedToWindow() 
{ 
	this->SetInvocationMessage(new BMessage(kMSG_THREADLIST_INVOCATION));
	this->SetTarget(this);
	fTreadRunner->Init(this, fStopSem);
	BColumnListView::AttachedToWindow(); 
} 
////////////////////////////////////////////////////////////////////////////////
// MessageReceived
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::MessageReceived (BMessage * msg)
{
	switch (msg->what){
		case kMSG_OPEN_BOARD:
			this->OpenBoard(msg);
			break;

		case kMSG_THREADLIST_INVOCATION:
			this->ListInvoked();
			break;

		case kMENU_THREADLIST_REFREASH:
			if(fBoardUri.Length()){
				findcachedir(fCacheDir, kCACHE_DIR_NAME, fBoardUri.String());
				this->LoadList(false);
			}
			break;

		//検索：zeta 2 スレ644のBe名無しさんより。
		case kMSG_FIND:
			this->DoFind(msg);
			break;		

		//既読の更新：zeta 2 スレ643のBe名無しさんより。
		case kMSG_UPDATE_THREADCOUNT:
			this->DoUpdate(msg);
			break;			

		case kUPDATEPREFS:
			break;

		default:
			BColumnListView::MessageReceived (msg);
			break;
	}
}
////////////////////////////////////////////////////////////////////////////////
// OpenBoard
// スレ一覧の表示を行う。
// 同時に指定スレを開くことも可能。
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::OpenBoard (BMessage * msg)
{
	const char *uri;
	status_t sts = msg->FindString("uri", &uri);
	if(sts != B_OK) return;
	fBoardUri.SetTo(uri);
	findcachedir(fCacheDir, kCACHE_DIR_NAME, fBoardUri.String());
	this->LoadList(theSettings().GetBool(kSETTING_OFFLINE), msg->FindString("dat"));
}
////////////////////////////////////////////////////////////////////////////////
// CurrentQuery
// zeta 2 スレ@713のBe名無しさんより。
//
////////////////////////////////////////////////////////////////////////////////
bool
ThreadListView::CurrentQuery (BMessage *msg)
{
	if (!fBoardUri.Length()) return false;
	BRow* row = this->CurrentSelection();
	if (!row) return false;
	if(!msg) return true;
	
	msg->AddInt32("no", static_cast<BIntegerField *>(row->GetField(COL_NO))->Value());				
	msg->AddInt32("read", static_cast<BIntegerField *>(row->GetField(COL_READ))->Value());				
	msg->AddString("title", static_cast<BStringField *>(row->GetField(COL_THREADNAME))->String());				
	msg->AddString("dat", static_cast<BStringField *>(row->GetField(COL_DATNAME))->String());				
	msg->AddString("host", fBoardUri);
	const char *dat = static_cast<BStringField *>(row->GetField(COL_DATNAME))->String();
	BString url;
	url << fBoardUri << dat;
	msg->AddString("url", url);
	return true;
}
////////////////////////////////////////////////////////////////////////////////
// ListInvoked
// 
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::ListInvoked ()
{
	BMessage openMessage(kMSG_OPEN_THREAD);
	this->CurrentQuery(&openMessage);
	BMessenger(this->Window()).SendMessage(&openMessage);
}
////////////////////////////////////////////////////////////////////////////////
// FindRowStringField
// 検索。
// Zeta 2スレ643のBe名無しさんより
////////////////////////////////////////////////////////////////////////////////
BRow*
ThreadListView::FindRowStringField(const char *label, int32 fieldNo)
{
	const int32 count = this->CountRows();
	int32 index = this->IndexOf(this->CurrentSelection());
	
	//カレントセレクションからサーチ
	for (int32 i = index + 1; i < count; i++) {
	BStringField *dat = static_cast<BStringField*>(RowAt(i)->GetField(fieldNo));
	if (dat && strstr(dat->String(), label) )
		return RowAt(i);
	}
	//カレントセレクションへ戻る。
	for (int32 i = 0; i <= index; i++) {
	BStringField *dat = static_cast<BStringField*>(RowAt(i)->GetField(fieldNo));
	if (dat && strstr(dat->String(), label) )
		return RowAt(i);
	}
	return NULL;
}
////////////////////////////////////////////////////////////////////////////////
// DoFind
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::DoFind(BMessage *msg)
{
	BRow *row = FindRowStringField(msg->FindString("findthis"), COL_THREADNAME);
	if (row) {
		this->SetFocusRow(row, true);
		this->ScrollTo(row);
	}else{
		beep();
	}
}
////////////////////////////////////////////////////////////////////////////////
// DoUpdate
//
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::DoUpdate(BMessage *msg)
{
	BRow *row = this->FindRowStringField(msg->FindString("dat"), COL_DATNAME);
	if (!row)	return;
	
	int32 thrdno = msg->FindInt32("thread_no");
	BIntegerField* field = static_cast<BIntegerField*>(row->GetField(COL_READ));
	field->SetValue(thrdno);
	field = static_cast<BIntegerField*>(row->GetField(COL_RES));
	if (field->Value() < thrdno) field->SetValue(thrdno);
	//20060701
	BBitmapField *bfield = static_cast<BBitmapField*>(row->GetField(COL_STATUS));
	bfield->SetBitmap(GetPositinArrow(1, 1));
	//20060509
	this->Refresh();
}
////////////////////////////////////////////////////////////////////////////////
// LoadSubjectFile
//
// TODO:将来的にBBSHandlerに移動。
////////////////////////////////////////////////////////////////////////////////
status_t
ThreadListView::LoadSubjectFile(BFile *file)
{
	return file->SetTo(fCacheDir, "subject.txt", B_READ_WRITE );
}
////////////////////////////////////////////////////////////////////////////////
// MakeLocalList
//
// TODO:将来的にBBSHandlerに移動。
// TODO:FileでなくBMallocIOを使えないか？
////////////////////////////////////////////////////////////////////////////////
status_t
ThreadListView::MakeLocalList(BFile *file)
{
BEntry ent;
BNode node;
char buf[2048];
int32 i = 0;
BString name;
char datname[B_FILE_NAME_LENGTH];

int32 num;
	fCacheDir->CreateFile("subject2.txt", file);
	while(fCacheDir->GetNextEntry(&ent) == B_OK){
	 	if (node.SetTo(&ent) != B_OK ) continue;
	 	if (!node.IsFile()) continue;
		ent.GetName(datname);
		if(strstr(datname, ".dat") == NULL && strstr(datname, ".cgi") == NULL) continue;

		node.ReadAttrString(DAT_ATTR_TITLE, &name);
		num = 0;
		node.ReadAttr(DAT_ATTR_THREADCOUNT, B_INT32_TYPE, 0, &num,  sizeof(int32)); 
		int n = sprintf(buf,  "%s<>%s(%ld)\n", datname, name.String(), num);
		file->Write(buf, n);
		i++;
	} 
	file->Seek(0, SEEK_SET);
	return i > 0 ? B_OK : B_ERROR;
}
////////////////////////////////////////////////////////////////////////////////
// status_t ThreadListView::LoadSubectFromINet(BFile &file)
// ネットからsubject.txtを読み込む。
// TODO:将来的にBBSHandlerに移動。
////////////////////////////////////////////////////////////////////////////////
status_t
ThreadListView::LoadSubectFromINet(BFile *file)
{
	if (static_cast<a2chViewer *>(be_app)->OffLineAlert() == B_ERROR)
		return B_ERROR;

	http_info hinfo;
	hinfo.init(true);
//	hinfo.body_length = 0;
//	hinfo.modified = "";

	if(this->LoadSubjectFile(file) == B_OK){
		file->ReadAttrString("Last Modified", &hinfo.modified);
	}

	BMallocIO outbuf;
	outbuf.SetBlockSize(1024); //20060803

	char host[64], subdir[64], filename[64];
	uri2host(fBoardUri.String(), host, subdir);
	sprintf(filename,"%s/subject.txt", subdir);

	status_t sts;
	sts = get(host, filename, outbuf, hinfo);

	if(sts == HTTP_NOT_MODIFIED) return B_OK;
	if(sts != HTTP_OK) return B_ERROR; 
	
	fCacheDir->CreateFile("subject.txt", file);
	
	file->Write(outbuf.Buffer(), outbuf.BufferLength());
	file->WriteAttrString("Last Modified", &hinfo.modified);
	
	file->Seek(0, SEEK_SET);
	return B_OK;
}
////////////////////////////////////////////////////////////////////////////////
// LoadList
// スレ一覧リストを作成。datnameがある場合は指定されたスレも開く。
//
////////////////////////////////////////////////////////////////////////////////
void
ThreadListView::LoadList(bool uselocal, const char* datname)
{
fDatFile = new BFile;
status_t sts = B_OK;

	fTreadRunner->Stop();
	this->Clear();
	if(!strcmp((static_cast<a2chViewer *>(be_app)->CurReader())->GetName(), "ローカルデータ")){
		if (this->MakeLocalList(fDatFile) != B_OK) {
			(new BAlert("","ローカルDatファイルがありません。", "OK"))->Go();
			return;
		}
	}else{
		if(uselocal){
			if( this->LoadSubjectFile(fDatFile) != B_OK){
				if(static_cast<a2chViewer *>(be_app)->Alert() == B_OK)
					sts = this->LoadSubectFromINet(fDatFile);
				else{
					(new BAlert("","ファイルをロードできません。", "OK"))->Go();
					return;
				}
			}
		}else{
			sts = this->LoadSubectFromINet(fDatFile);
		}
		//2005/05/03
		if(sts == B_ERROR){
			(new BAlert("","サーバーに接続できません", "OK"))->Go();
			return;
		}
	}
	
	fOpenDatName.SetTo(datname);
	fTreadRunner->Run();
}
////////////////////////////////////////////////////////////////////////////////
// ThreadListView::SetArrow
//
//
////////////////////////////////////////////////////////////////////////////////
BBitmap *
ThreadListView::GetPositinArrow(int32 newposition, int32 oldposion)
{
	if(oldposion <= 0)
		return NULL;

	if(newposition < oldposion){
		return fBlueUpArrow;
	}else if(newposition == oldposion){
		return fYellowEqaulArrow;
	}else{
		return fRedDownArrow;
	}	
}
////////////////////////////////////////////////////////////////////////////////
//
//
////////////////////////////////////////////////////////////////////////////////
BRow*
makeThreadRow(ThreadListView *fView, char *title, char *datname, int resnum, int threadposion)
{
BRow *row = new BRow();

	BEntry ent;
	BNode node; 		
	fView->fCacheDir->FindEntry(datname, &ent);
	node.SetTo(&ent);

	//スレ順
	row->SetField (new BIntegerField (threadposion), COL_NO);

	//スレの上下ステータス
	int32 oldthreadposion = 0;
	if (ent.Exists()){
		node.ReadAttr(DAT_ATTR_THREADNO, B_INT32_TYPE, 0, &oldthreadposion, sizeof(int32));
	}
	row->SetField (new BBitmapField (fView->GetPositinArrow(threadposion, oldthreadposion)), COL_STATUS);

	if (ent.Exists()){
		node.WriteAttr(DAT_ATTR_THREADNO, B_INT32_TYPE, 0, &threadposion, sizeof(int32));
	}

	//タイトル
	row->SetField (new BStringField (title), COL_THREADNAME);

	//レス数
	row->SetField (new BIntegerField (resnum), COL_RES);

	//既読
	int32 thread_read_no = 0;
	if (ent.Exists()){
		node.ReadAttr(DAT_ATTR_THREADCOUNT, B_INT32_TYPE, 0, &thread_read_no, sizeof(int32));
	}
	row->SetField (new BIntegerField (thread_read_no), COL_READ);

	//更新日付
	row->SetField (new BStringField (""), COL_DATE);

	//datファイル名
	row->SetField (new BStringField (datname), COL_DATNAME);
return row;
}
////////////////////////////////////////////////////////////////////////////////
// ThreadListView::Reader::Run
//20060510 lock見直し
//
////////////////////////////////////////////////////////////////////////////////
status_t
ThreadListView::Run(void *_this)
{
#define NAMEBUFSIZ 256
char datname[NAMEBUFSIZ], title[NAMEBUFSIZ*2];
int tn = 0;

	aThread<ThreadListView> *reader = static_cast<aThread<ThreadListView> *>(_this);
	BRow *invokedRow = NULL;
	
	if (!reader->Lock())
		return B_INTERRUPTED;

	ThreadListView *view = reader->View();
	BFile *file = view->fDatFile;

	BMallocIO utf8;
	utf8.SetBlockSize(1024); //20060803

	if(!strcmp((static_cast<a2chViewer *>(be_app)->CurReader())->GetName(), "ローカルデータ")){
		char buf[1024];
		ssize_t n;
		while(1){
			n = file->Read(buf, sizeof(buf));
			if(n <= 0) break;
			utf8.Write(buf, n);
		}
	}else{
		cutf8(file, &utf8);
	}
	delete file;
	const char *s = static_cast<const char *>(utf8.Buffer());
	const char *se = s + utf8.BufferLength();
	// Zeta R5スレ@399のBe名無しさんより、
	// subject.txtがうまく変換できずゴミが最後に付加されることがあるようです。
	while (*se != '\n') se--; 

	while(s < se){
		if(view->fTreadRunner->StopLoading()) goto done;

		char *p = datname;
		// get dat
		//20051209:machi bbs対応。
		while (*s != 't' && *s != 'i') *p++ = *s++;
		*p++ = *s;
		*p = 0;
		
		// skip delimter
		if (*(s + 1) == '<'){
			s += 3; //skip "t<>"
		}else{
			s += 2; //skip "t "
		}

		const char *lf = strchr(s, '\n');
		if (lf == NULL) lf = se;	// 多分、そんなことは無いと思うが
		
		const char *gt = lf;
		while(*gt != ')' && s < gt) --gt;
		const char *lt = gt;
		while(*lt != '(' && s < lt) --lt;
		int threadnum = 0;
		if (lt[0] == '(' && gt[0] == ')') {
			for (const char *dt = lt+1; dt < gt; dt++)
			 threadnum = threadnum * 10 + (dt[0]-'0');
		} else lt = lf;
		int length = (lt-s < (int)sizeof(title)-2)
							 ?  lt-s : sizeof(title)-2;
		memcpy(title, s, length);
		title[length] = '\0';
		s = lf+1;

		tn++;

		BRow *row = makeThreadRow(view, title, datname, threadnum, tn);

		view->Window()->Lock();
		view->AddRow (row);
		view->Window()->Unlock();
		if (!invokedRow && view->fOpenDatName.Length()) {
			if(!view->fOpenDatName.Compare(datname)){
				invokedRow = row;
				view->Window()->Lock();
				view->SetFocusRow(invokedRow, true);
				view->ListInvoked();
				view->Window()->Unlock();
			}
		}
	}

	//スレが指定されてない場合は一覧先頭にスクロール。
	if (!view->fOpenDatName.Length()) {
		view->Window()->Lock();
		view->ScrollTo(view->RowAt(static_cast<int32>(0)));
		view->Window()->Unlock();
	}else{
		if(!invokedRow){
			beep();
			(new BAlert("","指定スレがありません。","Oh!"))->Go();
		}else{
			//指定スレにスクロール
			view->Window()->Lock();
			view->ScrollTo(invokedRow);
			view->Window()->Unlock();
		}
	}
	
done:
	//20060913
	view->Window()->Lock();
	view->Refresh();
	view->Window()->Unlock();

	reader->Unlock();
	return B_NO_ERROR;
}
